package edu.washington.cs.workflow.analysis.guistructure;

import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.Map;
import java.util.Set;

import javax.swing.JMenuItem;
import javax.swing.MenuElement;

import org.uispec4j.MenuItem;
import org.uispec4j.Panel;
import org.uispec4j.UIComponent;
import org.uispec4j.UISpec4J;

import edu.washington.cs.workflow.analysis.guistructure.uievents.MenuItemEvent;
import edu.washington.cs.workflow.analysis.guistructure.uievents.UIEvent;
import edu.washington.cs.workflow.analysis.guistructure.uievents.UIEvent.WINDOW;
import edu.washington.cs.workflow.analysis.guistructure.uievents.UIEventFactory;
import edu.washington.cs.workflow.analysis.guistructure.uievents.UIEventOutcome;
import edu.washington.cs.workflow.util.Utils;

/**
 * This implements the feedback-directed test generation technique for GUI.
 * By default, it generates 10 tests or runs 10 seconds
 * 
 * XXX
 * Limitation:
 * (1) handle error dialog
 * (2) limited widget support
 * */
public class FeedbackDirectedTester {

	static {
		UISpec4J.init();
	}
	
	private Collection<EventSequence> pool = new LinkedList<EventSequence>();
	private Collection<EventSequence> outputSeqs = new LinkedList<EventSequence>();
	private Collection<EventSequence> errorSeqs = new LinkedList<EventSequence>();
	
	public final String mainClassName;
	public final Class<?> mainClass;
	public final String[] args;
	
	private int windowsPopTimeLimit = 1000;
	private int maxsize = 20;
	
	private int testlimit = 10;
	private long timelimit = 10000; //mili-seconds
	
	private long teststarttime = 0l;
	
	public FeedbackDirectedTester(String mainClassName, String[] args) {
		Utils.checkNotNull(mainClassName);
		Utils.checkNotNull(args);
		this.mainClassName = mainClassName;
		this.args = args;
		try {
		    mainClass = Class.forName(mainClassName);
		} catch (Throwable e) {
			throw new Error(e);
		}
		//set the time limit
		UISpec4J.setWindowInterceptionTimeLimit(windowsPopTimeLimit);
	}
	
	public void setMaxsize(int size) {
		Utils.checkTrue(maxsize > 0, "Wrong size value: " + maxsize);
		this.maxsize = size;
	}
	
	public void setTimelimit(long limit) {
		Utils.checkTrue(limit > 0, "Wrong time limit: " + limit);
		this.timelimit = limit;
	}
	
	public void setTestlimit(int testlimit) {
		Utils.checkTrue(testlimit > 0, "Wrong testlimit value: " + testlimit);
		this.testlimit = testlimit;
	}
	
	public void setWindowsPopTimeLimit(int timelimit) {
		Utils.checkTrue(timelimit > 0, "Wrong time limit value: " + timelimit);
		this.windowsPopTimeLimit = timelimit;
	}
	
	public Collection<EventSequence> generateEventSequences() {
		this.populateInitPool();
		Utils.debugPrintln("Finish populating pool with the init sequences.");
		Utils.checkTrue(!this.pool.isEmpty(), "The sequence pool should not be empty.");

		//record the time when test generation starts
		this.teststarttime = System.currentTimeMillis();
		while (!shouldStop() && !enoughTests()) {
			//randomly pick a sequence from the pool
			EventSequence seq = Utils.randomElement(this.pool);
			//get a fresh window and replay the event sequence
			Panel mainPanel = this.getFreshMainWindow();
			EventSequenceReplayer replayer = new EventSequenceReplayer(seq, mainPanel);
			SequenceExecutionOutcome outcome = replayer.replay();
			//there must not be any exception during replay before extending
			if(outcome.getException() != null) {
				 //a non-deterministic error could happen while replaying twice, e.g., 
				 //the menu: load new crossword to solve in Crossword Sage
				//this.pool.remove(seq);, do not remove, it may be useful later
				this.errorSeqs.add(seq);
				continue;
			}
			Utils.checkTrue(outcome.getException() == null);
			Collection<Panel> activePanels = outcome.getActivePanels();
			//if there is any active panels which we can continue generating tests
			if (!activePanels.isEmpty()) {
				Panel nextPanel = Utils.randomElement(activePanels);
				//random issue an event, note that some event may be invalid (null),
				//such as click on a panel
				UIEvent uiEvent = Spec4JUtils.nextRandomUIEvent(nextPanel); //, allComps);
				//can be null
				if(uiEvent == null) {
					continue;
				}
				boolean canFire = uiEvent.canFireEvent();
				if (uiEvent != null && canFire) {
					Utils.debugPrintln("extending a seq, ready to fire: " + uiEvent.toString() + " for extension, "
							+ " event can fire? : " + canFire);
					//probe the result of this UI event, save the window pop up info
					uiEvent.probeWindowInfo();
					//replay the seq for a second time, this time we know the windows status before uiEvent
					//gets executed. but need to reach a proper status
					Panel newMainPanel = this.getFreshMainWindow();
					EventSequenceReplayer newReplayer = new EventSequenceReplayer(seq, newMainPanel);
					SequenceExecutionOutcome newOutcome = newReplayer.replay(); //replay twice
					Utils.checkTrue(newOutcome.getException() == null);
					Collection<Panel> newActivePanels = newOutcome.getActivePanels();
					Utils.checkTrue(!newActivePanels.isEmpty());
					Panel newNextPanel = Spec4JUtils.getMatchedPanel(nextPanel, newActivePanels);
					//XXX FIXME
					if(newNextPanel == null) {
						this.errorSeqs.add(seq);
						continue;
					}
					Utils.checkNotNull(newNextPanel);
					UIComponent newComponent = Spec4JUtils.getMatchedComponent(uiEvent, nextPanel/*edited*/, newNextPanel);
					Utils.checkNotNull(newComponent, "fail to find: " + uiEvent.getUIComponent().getDescription()
							+ ", in panel: " + newNextPanel.getDescription());
					UIEvent newUIEvent = UIEventFactory.replicateExecutedEvent(uiEvent, newComponent, newNextPanel);
					UIEventOutcome eventOutcome = newUIEvent.fireEvent();
					
					//get the outcome of extending a sequence
					if (eventOutcome == null) {
						//the operation is meaningless, such as setting a value on a textfield which is not editable.
						Utils.debugPrintln("In extending a sequence, get an illegal operations after firing: " + uiEvent);
					} else {
						// to see if it has some error or it is still legal
						EventSequence newSeq = seq.extend(uiEvent);
						if (eventOutcome.isSuccess()) {
							newSeq.setOutcome(new NormalExecution());
							this.pool.add(newSeq);
						} else {
							newSeq.setOutcome(new ExceptionalExecution(eventOutcome.getError()));
							this.errorSeqs.add(newSeq);
						}
					}
				}
			}
		}

		return outputSeqs;
	}
	
	private boolean shouldStop() {
		return (System.currentTimeMillis() - this.teststarttime > this.timelimit);
	}
	
	private boolean enoughTests() {
		return this.pool.size() > this.testlimit;
	}
	
	
	
//	private UIEvent nextRandomNonNullUIEvent(Panel nextPanel, Collection<UIComponent> allComps) {
//		Collection<UIEvent> allNonNulls = new LinkedList<UIEvent>();
//		for(UIComponent comp : allComps) {
//			UIEvent uiEvent = UIEventFactory.createEvent(comp, nextPanel);
//			if(uiEvent != null) {
//				allNonNulls.add(uiEvent);
//			}
//		}
//		if(allNonNulls.isEmpty()) {
//			return null;
//		} else {
//			return Utils.randomElement(allNonNulls);
//		}
//	}
	
	private Panel getFreshMainWindow() {
		return MainWindowCreator.getFreshMainWindow(this.mainClass, this.args);
	}
	
	private void populateInitPool() {
		Panel main = this.getFreshMainWindow();
//		System.out.println(main.getDescription());
//		System.exit(1);
		Collection<UIEvent> allInitEvents = Spec4JUtils.allNonNullRandomUIEvents(main);
		
//		for(UIEvent e : allInitEvents) {
//			System.out.println(e);
//			System.out.println(e.getClass());
//			System.out.println("   " + e.canFireEvent());
//		}
//		System.exit(1);
		
		Utils.debugPrintln("Number of init events: " + allInitEvents.size());
		
	    for(UIEvent event : allInitEvents) {
			//check if it can fire or not, e.g., an textbox which is not editable cannot trigger an event
			if(!event.canFireEvent()) {
				continue;
			}

			Utils.debugPrintln("fire event: " + event.getUIComponent().getLabel());
			
			//probe the window info, save the info that whether an event can trigger a window
			//after getting this info, "replay" this event on a fresh main window
			event.probeWindowInfo();
			Panel newPanel = this.getFreshMainWindow();
			UIComponent newMatchedUIC = Spec4JUtils.getMatchedComponent(event, main/*edited*/, newPanel);
			Utils.checkNotNull(newMatchedUIC, "Cannot find: " + event.getUIComponent().getDescription() + ", in main: " + main.getDescription()
					+ ", the pos is: " + event.getUIComponentPos());
			UIEvent newEvent = UIEventFactory.replicateExecutedEvent(event, newMatchedUIC, newPanel);

			//fire the newly replicated event, with the window flag
			UIEventOutcome outcome = newEvent.fireEvent();
			
			if(outcome == null) { //illegal operation, such as set text on a textbox which is not editable
				Utils.debugPrintln("Fire an illegal operation in initing the pool: " + newEvent);
				continue;
			}
			Utils.debugPrintln("Result of outcome in initing the pool: " + outcome.toString());
            EventSequence seq = new EventSequence();
			seq.addUIEvent(event);
			if(!outcome.isSuccess()) {
				Throwable e = outcome.getError();
				seq.setOutcome(new ExceptionalExecution(e));
				this.errorSeqs.add(seq);
			} else {
				seq.setOutcome(new NormalExecution());
				this.pool.add(seq);
			}
		}
	}
	
	public static int testNum = 50;
	public static int timeLimit = 1000000;
	public static boolean printMap = false;
	public static boolean turnOnTrace = true;
	public static int popupWindowTime = 1000;
	
	public static void main(String[] args) {
		FeedbackDirectedTester tester = new FeedbackDirectedTester(args[0], new String[]{});
//		FeedbackDirectedTester tester = new FeedbackDirectedTester("samples.calculator.CalculatorPanel", new String[]{});
//		FeedbackDirectedTester tester = new FeedbackDirectedTester("sample.gui.SimpleUIForTest", new String[]{});
		tester.setTestlimit(testNum);
		tester.setTimelimit(timeLimit);
		tester.setWindowsPopTimeLimit(popupWindowTime);
		tester.generateEventSequences();
		
		Utils.debugPrintln(" here is all legal sequences, size: " + tester.pool.size());
		int index = 1;
		for(EventSequence seq : tester.pool) {
			Utils.debugPrintln(index++);
			Utils.debugPrintln(seq.toString());
		}
		
		Utils.debugPrintln(" here is all error sequences, size: " + tester.errorSeqs.size());
		index = 1;
		for(EventSequence seq : tester.errorSeqs) {
			Utils.debugPrintln(index++);
			Utils.debugPrintln(seq.toString());
		}
		
		//for debugging
		UIActionTracer.tracer.setTurnOn(turnOnTrace);
//		UIActionTracer.tracer.dumpTraces();
//		Utils.debugPrintln("------------------");
//		if (printMap) {
//			Map<UIActionEntity, Collection<UIActionHandler>> map1 = UIActionTracer.tracer.getAnalyzer().createActionToHandlerMap();
//			for (UIActionEntity e : map1.keySet()) {
//				Utils.debugPrintln(e);
//				Utils.debugPrintln("    " + map1.get(e));
//			}
//			Utils.debugPrintln("------------------");
//			Map<UIActionHandler, Collection<UIActionEntity>> map2 = UIActionTracer.tracer.getAnalyzer().createHandlerToActionMap();
//			for (UIActionHandler h : map2.keySet()) {
//				Utils.debugPrintln(h);
//				Utils.debugPrintln("    " + map2.get(h));
//			}
//		}
	}
}
